<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <title>自己用自己的骰子，我大富翁和飞行棋无敌了！</title>
    <style>
        html,
        body {
            padding: 0;
            margin: 0;
        }

        body {
            height: 100vh;
            display: flex;
            align-items: end;
            justify-content: center;
        }

        .container {
            width: 100%;
            height: 100%;
            background-image: linear-gradient(#6dd5fa, #2980b9);
        }

        .lil-gui {
            --width: 450px;
            max-width: 90%;
            --widget-height: 20px;
            font-size: 15px;
            --input-font-size: 15px;
            --padding: 10px;
            --spacing: 10px;
            --slider-knob-width: 5px;
            --background-color: rgba(5, 0, 15, 0.8);
            --widget-color: rgba(255, 255, 255, 0.3);
            --focus-color: rgba(255, 255, 255, 0.4);
            --hover-color: rgba(255, 255, 255, 0.5);
            --font-family: monospace;
        }
    </style>
</head>

<body>
    <div class="container">
        <canvas id="canvas"></canvas>
    </div>
    <script type="importmap">
        {"imports":{"three":"https://unpkg.com/three@0.164.0/build/three.module.js","three/addons/":"https://unpkg.com/three@0.164.0/examples/jsm/"}}
    </script>
    <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>
    <script
        type="module">import *as CANNON from 'https://cdn.skypack.dev/cannon-es'; import *as THREE from 'three'; import *as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; import GUI from 'https://cdn.jsdelivr.net/npm/lil-gui@0.18.2/+esm'; const containerEl = document.querySelector('.container'); const canvasEl = document.querySelector('#canvas'); let renderer, scene, camera, diceMesh, physicsRender, simulation; let simulationOn = true; let currentResult = [0, 0]; const params = { segments: 40, edgeRadius: 0.08, notchRadius: 0.15, notchDepth: 0.17, restitution: 0.3, friction: 0.1, desiredResult: 7, throw: throwMe, }; function throwMe() { simulationOn = true; throwDice(); } const diceArray = []; const floorPlanesArray = []; let throwBtn; initPhysics(); initScene(); createFloor(); diceMesh = createDiceMesh(); for (let i = 0; i < 2; i++) { diceArray.push(createDice()); addDiceEvents(diceArray[i], i) } createControls(); throwMe(); render(); window.addEventListener('resize', updateSceneSize); window.addEventListener('click', () => { }); function initScene() { renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, canvas: canvasEl, }); renderer.shadowMap.enabled = true; renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(45, containerEl.clientWidth / containerEl.clientHeight, 0.1, 100); camera.position.set(0, 9, 12); camera.lookAt(0, 4, 0); updateSceneSize(); const ambientLight = new THREE.AmbientLight(0xffffff, 1); scene.add(ambientLight); const light = new THREE.PointLight(0xffffff, 1000); light.position.set(10, 20, 5); light.castShadow = true; light.shadow.mapSize.width = 2048; light.shadow.mapSize.height = 2048; scene.add(light) } function initPhysics() { const gravity = new CANNON.Vec3(0, -50, 0); const allowSleep = true; physicsRender = new CANNON.World({ allowSleep, gravity, }); simulation = new CANNON.World({ allowSleep, gravity, }); physicsRender.defaultContactMaterial.restitution = params.restitution; simulation.defaultContactMaterial.restitution = params.restitution; physicsRender.defaultContactMaterial.friction = params.friction; simulation.defaultContactMaterial.friction = params.friction } function createFloor() { for (let i = 0; i < 4; i++) { const body = new CANNON.Body({ type: CANNON.Body.STATIC, shape: new CANNON.Plane(), }); physicsRender.addBody(body); simulation.addBody(body); let mesh; if (i === 0) { mesh = new THREE.Mesh(new THREE.PlaneGeometry(100, 100, 100, 100), new THREE.ShadowMaterial({ opacity: 0.1, })); scene.add(mesh); mesh.receiveShadow = true } floorPlanesArray.push({ body, mesh, }) } floorPositionUpdate() } function floorPositionUpdate() { floorPlanesArray.forEach((f, fIdx) => { if (fIdx === 0) { f.body.position.y = 0; f.body.quaternion.setFromEuler(-0.5 * Math.PI, 0, 0) } else if (fIdx === 1) { f.body.quaternion.setFromEuler(0, 0.5 * Math.PI, 0); f.body.position.x = (-6 * containerEl.clientWidth) / containerEl.clientHeight } else if (fIdx === 2) { f.body.quaternion.setFromEuler(0, -0.5 * Math.PI, 0); f.body.position.x = (6 * containerEl.clientWidth) / containerEl.clientHeight } else if (fIdx === 3) { f.body.quaternion.setFromEuler(0, Math.PI, 0); f.body.position.z = 3 } if (f.mesh) { f.mesh.position.copy(f.body.position); f.mesh.quaternion.copy(f.body.quaternion) } }) } function createDiceMesh() { const boxMaterialOuter = new THREE.MeshStandardMaterial({ color: 0xffffff, }); const boxMaterialInner = new THREE.MeshStandardMaterial({ color: 0x000000, roughness: 0, metalness: 1, }); const g = new THREE.Group(); const innerSide = 1 - params.edgeRadius; const innerMesh = new THREE.Mesh(new THREE.BoxGeometry(innerSide, innerSide, innerSide), boxMaterialInner); const outerMesh = new THREE.Mesh(createBoxGeometry(), boxMaterialOuter); outerMesh.castShadow = true; g.add(innerMesh, outerMesh); return g } function createDice() { const mesh = diceMesh.clone(); scene.add(mesh); const shape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5)); const mass = 1; const sleepTimeLimit = 0.02; const body = new CANNON.Body({ mass, shape, sleepTimeLimit, }); physicsRender.addBody(body); const simulationBody = new CANNON.Body({ mass, shape, sleepTimeLimit, }); simulation.addBody(simulationBody); return { mesh, body: [body, simulationBody], startPos: [null, null, null], } } function createBoxGeometry() { let boxGeometry = new THREE.BoxGeometry(1, 1, 1, params.segments, params.segments, params.segments); const positionAttr = boxGeometry.attributes.position; const subCubeHalfSize = 0.5 - params.edgeRadius; const notchWave = (v) => { v = (1 / params.notchRadius) * v; v = Math.PI * Math.max(-1, Math.min(1, v)); return params.notchDepth * (Math.cos(v) + 1) }; const notch = (pos) => notchWave(pos[0]) * notchWave(pos[1]); for (let i = 0; i < positionAttr.count; i++) { let position = new THREE.Vector3().fromBufferAttribute(positionAttr, i); const subCube = new THREE.Vector3(Math.sign(position.x), Math.sign(position.y), Math.sign(position.z)).multiplyScalar(subCubeHalfSize); const addition = new THREE.Vector3().subVectors(position, subCube); if (Math.abs(position.x) > subCubeHalfSize && Math.abs(position.y) > subCubeHalfSize && Math.abs(position.z) > subCubeHalfSize) { addition.normalize().multiplyScalar(params.edgeRadius); position = subCube.add(addition) } else if (Math.abs(position.x) > subCubeHalfSize && Math.abs(position.y) > subCubeHalfSize) { addition.z = 0; addition.normalize().multiplyScalar(params.edgeRadius); position.x = subCube.x + addition.x; position.y = subCube.y + addition.y } else if (Math.abs(position.x) > subCubeHalfSize && Math.abs(position.z) > subCubeHalfSize) { addition.y = 0; addition.normalize().multiplyScalar(params.edgeRadius); position.x = subCube.x + addition.x; position.z = subCube.z + addition.z } else if (Math.abs(position.y) > subCubeHalfSize && Math.abs(position.z) > subCubeHalfSize) { addition.x = 0; addition.normalize().multiplyScalar(params.edgeRadius); position.y = subCube.y + addition.y; position.z = subCube.z + addition.z } const offset = 0.23; if (position.y === 0.5) { position.y -= notch([position.x, position.z]) } else if (position.x === 0.5) { position.x -= notch([position.y + offset, position.z + offset]); position.x -= notch([position.y - offset, position.z - offset]) } else if (position.z === 0.5) { position.z -= notch([position.x - offset, position.y + offset]); position.z -= notch([position.x, position.y]); position.z -= notch([position.x + offset, position.y - offset]) } else if (position.z === -0.5) { position.z += notch([position.x + offset, position.y + offset]); position.z += notch([position.x + offset, position.y - offset]); position.z += notch([position.x - offset, position.y + offset]); position.z += notch([position.x - offset, position.y - offset]) } else if (position.x === -0.5) { position.x += notch([position.y + offset, position.z + offset]); position.x += notch([position.y + offset, position.z - offset]); position.x += notch([position.y, position.z]); position.x += notch([position.y - offset, position.z + offset]); position.x += notch([position.y - offset, position.z - offset]) } else if (position.y === -0.5) { position.y += notch([position.x + offset, position.z + offset]); position.y += notch([position.x + offset, position.z]); position.y += notch([position.x + offset, position.z - offset]); position.y += notch([position.x - offset, position.z + offset]); position.y += notch([position.x - offset, position.z]); position.y += notch([position.x - offset, position.z - offset]) } positionAttr.setXYZ(i, position.x, position.y, position.z) } boxGeometry.deleteAttribute('normal'); boxGeometry.deleteAttribute('uv'); boxGeometry = BufferGeometryUtils.mergeVertices(boxGeometry); boxGeometry.computeVertexNormals(); return boxGeometry } function addDiceEvents(dice, diceIdx) { dice.body.forEach((b) => { b.addEventListener('sleep', (e) => { b.allowSleep = false; if (simulationOn) { const euler = new CANNON.Vec3(); e.target.quaternion.toEuler(euler); const eps = 0.1; let isZero = (angle) => Math.abs(angle) < eps; let isHalfPi = (angle) => Math.abs(angle - 0.5 * Math.PI) < eps; let isMinusHalfPi = (angle) => Math.abs(0.5 * Math.PI + angle) < eps; let isPiOrMinusPi = (angle) => Math.abs(Math.PI - angle) < eps || Math.abs(Math.PI + angle) < eps; if (isZero(euler.z)) { if (isZero(euler.x)) { currentResult[diceIdx] = 1 } else if (isHalfPi(euler.x)) { currentResult[diceIdx] = 4 } else if (isMinusHalfPi(euler.x)) { currentResult[diceIdx] = 3 } else if (isPiOrMinusPi(euler.x)) { currentResult[diceIdx] = 6 } else { b.allowSleep = true; throwDice(); } } else if (isHalfPi(euler.z)) { currentResult[diceIdx] = 2 } else if (isMinusHalfPi(euler.z)) { currentResult[diceIdx] = 5 } else { b.allowSleep = true; throwDice(); } const thisDiceRes = currentResult[diceIdx]; const anotherDiceRes = currentResult[diceIdx ? 0 : 1]; const currentSum = currentResult.reduce((a, v) => a + v, 0); if (anotherDiceRes === 0 && thisDiceRes >= params.desiredResult) { throwDice(); } else if (anotherDiceRes !== 0) { if (params.desiredResult !== currentSum) { throwDice(); } else { simulationOn = false; throwBtn.innerHTML = 'throw!'; throwDice(); } } } }) }) } function render() { if (simulationOn) { simulation.step(1 / 60, 5000, 60) } else { physicsRender.fixedStep(); for (const dice of diceArray) { dice.mesh.position.copy(dice.body[0].position); dice.mesh.quaternion.copy(dice.body[0].quaternion) } renderer.render(scene, camera) } requestAnimationFrame(render) } function updateSceneSize() { camera.aspect = containerEl.clientWidth / containerEl.clientHeight; camera.updateProjectionMatrix(); renderer.setSize(containerEl.clientWidth, containerEl.clientHeight); floorPositionUpdate() } function throwDice() { const quaternion = new THREE.Quaternion(); if (simulationOn) { throwBtn.innerHTML = 'calculating a throw...'; currentResult = [0, 0]; diceArray.forEach((d) => { d.startPos = [Math.random(), Math.random(), Math.random()] }) } diceArray.forEach((d, dIdx) => { quaternion.setFromEuler(new THREE.Euler(2 * Math.PI * d.startPos[0], 0, 2 * Math.PI * d.startPos[1])); const force = 6 + 3 * d.startPos[2]; const b = simulationOn ? d.body[1] : d.body[0]; b.position = new CANNON.Vec3(3, 5 + dIdx, 2); b.velocity.setZero(); b.angularVelocity.setZero(); b.applyImpulse(new CANNON.Vec3(-force, force, 0), new CANNON.Vec3(0, 0, -0.5)); b.quaternion.copy(quaternion); b.allowSleep = true }) } function createControls() { const gui = new GUI(); gui.add(params, 'desiredResult', 2, 12, 1).name('result'); const btnControl = gui.add(params, 'throw').name('throw!'); throwBtn = btnControl.domElement.querySelector('button > .name'); }</script>
</body>

</html>